package org.judo.database.profile;

import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;

import org.judo.database.TypeTranslator;
import org.judo.database.exceptions.DatabaseException;
import org.judo.database.exceptions.ORMException;
import org.judo.propertyobject.PropertyNotFoundException;
import org.judo.propertyobject.PropertyObject;
import org.judo.util.ConvenienceBase;
import org.judo.util.JavaNameConversion;
import org.judo.util.Util;

public class EntityProfiler
{
	private static HashMap<String,TableSchema> tables;
	
	/**
	 * @param tableName
	 * @param obj 
	 * @param con
	 * @return
	 * @throws DatabaseException
	 * @throws ORMException 
	 */
	public static TableSchema getTableSchema(String tableName,PropertyObject object, Connection con) 
		throws DatabaseException, ORMException
	{
		if (tables == null)
			init(con);
		
		TableSchema table = tables.get(tableName);
		return table;
	}
	
	
	public static TableSchema getTableSchema(String tableName,Connection con) 
		throws DatabaseException, ORMException
	{
		if (tables == null)
			init(con);
		
		TableSchema table = tables.get(tableName);
		return table;
	}
	
	public static TableSchema[] allTableNames(Connection con) 
		throws DatabaseException
	{
		if (tables == null)
			init(con);
		
		return tables.values().toArray(new TableSchema[0]);
	}
	
	protected static void init(Connection con) 
		throws DatabaseException
	{
		try
		{
			tables = new HashMap<String,TableSchema>();
			
			DatabaseMetaData metaData = con.getMetaData();		
			ResultSet rs = metaData.getTables(null,"%","%",new String[]{"TABLE"});
				
			while(rs.next())
			{
				TableSchema table = new TableSchema();
				table.tableName = rs.getString("TABLE_NAME");				
				
				tables.put(JavaNameConversion.getClassName(table.tableName), table);
				ResultSet colRS = metaData.getColumns(null,"%",table.tableName,"%");
				
				while (colRS.next())
				{
					ColumnSchema col = new ColumnSchema();
					
					col.name = colRS.getString("COLUMN_NAME");
					col.type = TypeTranslator.getJavaType(colRS.getInt("DATA_TYPE"));
					
					int nullable = colRS.getInt("NULLABLE");
					
					if (nullable == DatabaseMetaData.columnNoNulls)
						col.isNullable = false;
					else
						col.isNullable = true;
					
					table.columns.put(JavaNameConversion.getMemberName(col.name),col);
					table.columnList.add(col);
				}
				
				ResultSet pkrs = metaData.getPrimaryKeys("%","%",table.tableName);
				
				while (pkrs.next())
				{		
					String priamryKey = pkrs.getString("COLUMN_NAME");
					table.primaryKeys.add(priamryKey);
					
					ColumnSchema col = table.columns.get(JavaNameConversion.getMemberName(priamryKey));
					col.isPrimaryKey = true;
				}
			}
			
			Iterator names = tables.keySet().iterator();
			
			Statement stmnt = null;
			ResultSet metaRs = null;
			
			try
			{
				stmnt = con.createStatement();
				
				while (names.hasNext())
				{
					String tableName = (String)names.next();
					String sql = "select * from "+JavaNameConversion.getUnderscoreNameFromClass(tableName)+" where 1 = 0";
					
					metaRs = stmnt.executeQuery(sql);
					
					TableSchema table = (TableSchema)tables.get(tableName);
					ResultSetMetaData rsmd = metaRs.getMetaData();
					
					int count = rsmd.getColumnCount();
					
					for (int i=0;i<count;i++)
					{
						int col = i+1;
						String colName = rsmd.getColumnName(col);
						boolean isAutoIncrement = rsmd.isAutoIncrement(col);
						
						ColumnSchema column = table.columns.get(JavaNameConversion.getMemberName(colName));	
						column.isAutoIncrement =  isAutoIncrement;
					}
					
					/*
					 * TODO: see if table has any relationships
					 */
					
					metaRs.close();
				}
			}
			finally
			{
				if (stmnt != null)
					stmnt.close();
			}
		}
		catch(Exception e)
		{
			e.printStackTrace();
			throw new DatabaseException("Error getting database meta data: "+e);
		}
	}
	
	/**
	 *  
	 *  When determining the schema for an entity, the
	 *  EntityProfiler first looks to see if it has been
	 *  explicitly mapped.  It then checks to see what it
	 *  can learn from the database metadata.  Lastly it
	 *  performs reflection on the object to get implicit
	 *  and explicit schema information.
	 *  
	 *  TODO: Cache the results from profiling so it
	 *  only needs to happen once.
	 *  
	 *  TODO: Need to do some java to sql name
	 *  style conversion here
	 *  
	 * @param object
	 * @return
	 * @throws ORMException 
	 */
	public static EntitySchema getProfile(PropertyObject object,Connection con,String overrideName)
		throws DatabaseException, ORMException
	{
		EntitySchema entity = new EntitySchema();
		
		if (tables == null)
			init(con);
		
		String className= object.persistedName();
		
		if (overrideName != null)
			className = JavaNameConversion.getClassName(overrideName);
	
		TableSchema table = tables.get(className);
		
		if (table == null)
			throw new ORMException("Trying to map '"+className+"' to a database table but a table by this name does not exist in the database. You may need to use @TableName on the class to tell this mapper what table is the target to be mapped to. Also, remember that if you have a class named MyEntity, then it is expected that the table will be My_Entity or MY_ENTITY or something like that.  Note the relationship between the cammelback notation and the underscore in the table name.  If this pattern doesn't work for your needs please see the documnetation on how to map the table name to the class name manually.");
		
		TableSchema procTable = updateWithObjInfo(table,object);
		entity.tableSchema = procTable;
		
		return entity;
	}
	
	private static TableSchema updateWithObjInfo(TableSchema table,PropertyObject object) 
		throws ORMException
	{
		TableSchema procTable = new TableSchema();
		procTable.tableName = table.tableName;
		
		for (ColumnSchema column: table.columnList)
		{
			ColumnSchema procColumn = new ColumnSchema();
			
			procColumn.isAutoIncrement = column.isAutoIncrement;
			procColumn.isNullable = column.isNullable;
			procColumn.isPrimaryKey = column.isPrimaryKey;
			procColumn.isSequenced = column.isSequenced;
			procColumn.name = column.name;
			procColumn.sequenceName = column.sequenceName;
			procColumn.type = column.type;
			
			try
			{
				String javaName = JavaNameConversion.getMemberName(column.name);
				
				if (!object.propertyExists(javaName))
				{
					continue;
				}
				
				if (object.propertyExists(javaName) && !object.propIsPrimitive(javaName))
				{
					throw new ORMException("The column '"+column.name+"' has the same name as the property '"+column.name+"' in class '"+
							object.getClass().getCanonicalName()+"' but this property is not of a primitive type.");
				}
				
				procTable.columns.put(column.name,procColumn);
				procTable.columnList.add(procColumn);
			} 
			catch (PropertyNotFoundException e){
					continue;
			}
		}
		
		for (String key:table.primaryKeys)
		{
			String javaName = JavaNameConversion.getMemberName(key);
			
			if (!object.propertyExists(javaName))
					continue;
			
			procTable.primaryKeys.add(key);				
		}
		
		introspectPropertyObject(object,procTable);
		
		return procTable;
	}
	
	private static void introspectPropertyObject(PropertyObject object, TableSchema procTable) 
		throws ORMException
	{
		List<String> propNames = object.propNames();
		
		for (String propName: propNames)
		{
			try{
				if (!object.propIsPrimitive(propName))
					continue;
			} 
			catch (PropertyNotFoundException e1){
				throw new ORMException("Internal Judo Error: The property '"+propName+"' in class '"+
						object.getClass().getCanonicalName()+"' was expected to exist but does not");
			}	
			
			ColumnSchema column = procTable.columns.get(propName);
			
			if (column == null)
				continue;
		
			try{
				if (object.isPrimaryKey(propName))
					column.isPrimaryKey = true;
				
				if (object.isAutoIncrement(propName))
					column.isAutoIncrement = true;
				
				if (object.isNullable(propName))
					column.isNullable = true;
				
			} catch (PropertyNotFoundException e){
				throw new ORMException("Internal Judo Error.  Property '"+propName+"' was expected in "+object.getClass().getCanonicalName());
			}
		}
	}

	public static void dump()
	{
		Iterator schemas = tables.values().iterator();
		
		while (schemas.hasNext())
		{
			TableSchema table = (TableSchema)schemas.next();	
			Iterator columns = table.columns.values().iterator();
			
			System.out.println("\t"+table.tableName);
			
			while (columns.hasNext())
			{
				ColumnSchema column = (ColumnSchema)columns.next();
				
				System.out.println("\t\t"+column.name);
				System.out.println("\t\t\tSequence Name: "+column.sequenceName);
				System.out.println("\t\t\tAuto Increment: "+column.isAutoIncrement);
				System.out.println("\t\t\tNullable: "+column.isNullable);
				System.out.println("\t\t\tIs Primary Key: "+column.isPrimaryKey);
				System.out.println("\t\t\tData Type: "+column.type.getCanonicalName());
			}
			
			System.out.println("\tKeys");
			
			List keys = table.primaryKeys;
			
			for (int i=0;i<keys.size();i++)
			{
				String key = (String)keys.get(i);
				System.out.println("\t\t"+key);
			}
		}
	}
}
